﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net.Sockets;
using System.Threading;

namespace PyNet
{
    static class ChatEvents
    {
        public static int GenericChatEvent = 0x00;
        public static int JoinedChannel = 0x01;
        public static int UserInChannel = 0x02;
        public static int UserJoin = 0x03;
        public static int UserPart = 0x04;
        public static int UserEdit = 0x05;
        public static int UserTalk = 0x06;
        public static int UserWhisper = 0x07;
        public static int ServerInformation = 0x08;
        public static int ServerError = 0x09;
        public static int ServerBroadcast = 0x0A;
        public static int RequestEcho = 0x0B;
        public static int RequestQuit = 0x0C;
        public static int RequestFatalError = 0x0D;
    }

    static class UserFlags
    {
        public static int Normal = 0x00;
        public static int Operator = 0x01;
        public static int Administrator = 0x02; // Admins = Operators, but Operators != Admins
    }

    class clsClient
    {
        private Socket connection;
        public int ThreadNumber = 0;
        private Thread thread;
        private byte proto = 0;

        // General
        public int ClientID = 0;

        // Protocol
        public string protocolName = "Unidentified";
        public DateTime protocolSet = new DateTime();

        public DateTime protocolLastKeepAlive = new DateTime();
        public DateTime protocolLastSwitchChannel = new DateTime();
        public DateTime protocolLastChatEvent = new DateTime();
        private DateTime LastPingRequest = new DateTime(); //Used for timing not recognition
        private bool WantPingResponse = false;
        public DateTime protocolLastPing = new DateTime();

        // PyNet
        public string Username = "User";
        public int Flags = UserFlags.Normal;
        public int Channel = Program.GetServer.VoidID;
        public int Ping = 0;
        public string IPAddress = "0.0.0.0";

        // Battle.net
        public string BN_Username = "";
        public string BN_Channel = "";
        public string BN_Realm = "";
        public int BN_RealmID = 0;
        public int BN_Flags = 0x00;
        public string BN_Game = "CHAT";

        public clsClient(Socket clientSocket, int id)
        {
            connection = clientSocket;
            ThreadNumber = id;
            ClientID = id;
            Username = "User#" + id.ToString();
            protocolLastPing = DateTime.Now;
            IPAddress = connection.RemoteEndPoint.ToString();
            StartLoop();
        }

        private string LVFlags()
        {
            return ClientID.ToString();
        }

        private void RecognizeFlags()
        {
            Flags = UserFlags.Normal;
            string[] admins = clsFileSystem.ReadData("admins.txt").Split('\n');
            foreach (string admin in admins) {
                if (admin.ToLower() == Username.ToLower())
                    Flags += UserFlags.Administrator;
            }
        }

        public string StatString()
        {
            char s = (char) 2;
            int adminBool = 0;
            if (IsAdmin())
                adminBool = 1;
            return String.Join(s.ToString(), new string[] { BN_Username, BN_Channel, BN_RealmID.ToString(), BN_Realm, BN_Flags.ToString(), BN_Game, adminBool.ToString(), Ping.ToString() });

        }

        public bool IsOnline()
        {
            return connection.Connected;
        }

        public bool IsAdmin()
        {
            return ((Flags & UserFlags.Administrator) == UserFlags.Administrator);
        }

        public bool IsOperator()
        {
            return ((Flags & UserFlags.Operator) == UserFlags.Operator);
        }

        private bool StartLoop()
        {
            if (thread != null && thread.ThreadState == ThreadState.Running)
                return false;

            thread = new Thread((ThreadStart)Loop);
            thread.IsBackground = true;
            thread.Start();

            return true;
        }

        public void Write(string text)
        {
            Program.GetLogger.Write("DarkSlateGray", "<Thread " + ThreadNumber.ToString() + "> ", "Gray", text);
        }

        #region Send Events
        public void SendLoginReponse(byte result, string notes)
        {
            clsBuffer_Build packet = new clsBuffer_Build();
            packet.InsertByte(result);
            if (notes != "")
                packet.InsertCString(notes);
            SendData(packet.Build(0x01));
        }

        public void SendAccountCreateResponse(byte result, string notes)
        {
            clsBuffer_Build packet = new clsBuffer_Build();
            packet.InsertByte(result);
            if (notes != "")
                packet.InsertCString(notes);
            SendData(packet.Build(0x03));
        }

        public void SendVersionResponse(byte result, string info, string location)
        {
            clsBuffer_Build packet = new clsBuffer_Build();
            packet.InsertByte(result);
            packet.InsertCString(info);
            packet.InsertDWord(1);
            packet.InsertDWord(0x01);
            packet.InsertCString(location);
            SendData(packet.Build(0x05));
        }

        public void SendChatEvent(int id, string username, int flags, int mod, string data)
        {
            clsBuffer_Build packet = new clsBuffer_Build();
            string usernameField = username;
            if (usernameField == "")
                usernameField = "#" + Username;
            packet.InsertDWord(id);
            packet.InsertCString(username);
            packet.InsertDWord(flags);
            packet.InsertDWord(mod);
            packet.InsertCString(data);
            SendData(packet.Build(0x04));
        }

        public void SendChatEvent(int id, string username, string data)
        {
            SendChatEvent(id, username, 0, 0, data);
        }

        public void SendPingRequest(int cookie)
        {
            clsBuffer_Build packet = new clsBuffer_Build();
            packet.InsertDWord(cookie);
            SendData(packet.Build(0x06));
            LastPingRequest = DateTime.Now;
            protocolLastPing = DateTime.Now;
            WantPingResponse = true;
        }
        #endregion

        #region Socket Events
        public void SendData(byte[] data)
        {
            try
            {
                if (IsOnline())
                    connection.Send(data);
            }
            catch (SocketException e)
            {
                string reason = "Socket error on send: " + e.Message;
                Write(reason);
                Kill(reason);
            }
            catch (ObjectDisposedException o)
            {
                Write("Unable to send data: object waiting to be disposed");
            }
        }

        public bool Kill()
        {
            return Kill("");
        }

        public bool Kill(string reason)
        {
            if (reason != "")
            {
                Write("Closing connection: " + reason);
                // Tell them why they deserve to die!
                SendChatEvent(0x0C, "", reason);
            }
            connection.Close();
            thread.Abort();
            Program.GetConsole.RemoveListItem(Username);
            return true;
        }
        #endregion


        #region Chat Events
        public void ProcessTextEvent(string text)
        {
            clsChannel chan = Program.GetServer.Channels[Channel];
            if (text.StartsWith("/"))
            {
                string[] data = text.TrimStart('/').Split(' ');
                string c = data[0];
                string[] p = data.AsEnumerable().Skip(1).ToArray<string>();
                ProcessCommand(c, p);
            }
            else
            {
                chan.TalkUser(Username, text);
            }
        }

        private void Command_Join(string channel)
        {
            int target = Program.GetServer.FindChannel(channel);

            if (target == -1)
            {
                target = Program.GetServer.CreateChannel(channel, ChannelFlags.IsNormal, ClientID);
            }
            else
            {
                clsChannel channelObj = Program.GetServer.Channels[target];
                if (channelObj.IsPrivate() && (!IsAdmin()))
                {
                    Write("User does not have permission to enter channel");
                    SendChatEvent(ChatEvents.ServerError, "", "That channel is restricted.");
                    return;
                }
            }

            Program.GetServer.Channels[Channel].PartUser(this);
            Thread.Sleep(100);
            Program.GetServer.Channels[target].JoinUser(this);
        }

        private void Command_Whisper(string username, string message)
        {
            int target = Program.GetServer.FindUser(username);
            if (target != -1)
            {
                clsClient user = Program.GetServer.FindUserObject(target);
                // From source
                user.SendChatEvent(ChatEvents.UserWhisper, Username, Flags, 0x00, message);
                // To target
                SendChatEvent(ChatEvents.UserWhisper, user.Username, user.Flags, 0x01, message);
            }
            else
            {
                SendChatEvent(ChatEvents.ServerError, "", "That user is not logged on to this server.");
            }
        }

        private void Command_Whois(string username)
        {
            int target = Program.GetServer.FindUser(username);
            if (target != -1)
            {
                clsClient user = Program.GetServer.FindUserObject(target);
                SendChatEvent(ChatEvents.ServerInformation, "", FormatUserWhois(user));
            }
            else
            {
                SendChatEvent(ChatEvents.ServerError, "", "That user is not logged on to this server.");
            }
        }

        private string FormatUserWhois(clsClient user)
        {
            return Username + ": On Battle.net as " + user.BN_Username + " using " + user.BN_Game + " on " + user.BN_Realm + " in channel " + user.BN_Channel + ".";
        }

        private void Command_GetBNETUsers()
        {
            Dictionary<int, clsClient>  clients = Program.GetServer.GetClients();
            foreach (int i in clients.Keys)
            {
                clsClient user = clients[i];
                if (user.BN_Channel.ToLower() == BN_Channel.ToLower())
                {
                    SendChatEvent(ChatEvents.ServerInformation, "", "Channel: " + FormatUserWhois(user));
                } else if (user.BN_RealmID == BN_RealmID)
                {
                    SendChatEvent(ChatEvents.ServerInformation, "", "Realm: " + FormatUserWhois(user));
                } else if (user.BN_Game == BN_Game)
                {
                    SendChatEvent(ChatEvents.ServerInformation, "", "Game: " + FormatUserWhois(user));
                }
            }
        }

        private void ProcessCommand(string command, string[] parameters)
        {
            string paramDisplay = " - ";
            if (parameters.Length > 0)
                paramDisplay = String.Join(" | ", parameters);
            Write("Command: " + command + " [" + paramDisplay + "]");
            string rest = String.Join(" ", parameters);
            if (command == "join")
                Command_Join(rest);
            else if (command == "whisper")
                Command_Whisper(parameters[0], String.Join(" ", parameters.AsEnumerable().Skip(1).ToArray<string>()));
            else if (command == "bnet")
                Command_GetBNETUsers();
            else if (command == "whois")
                Command_Whois(rest);
                
        }

        private void ProcessChatEvent(int id, string data)
        {
            /*  0x01: Joined a channel
	            0x02: Sent once for everyone in your channel (ordered by seniority on the server)
	            0x03: User join
	            0x04: User leave
	            0x05: User change
	            0x06: User talk (includes you, won't spit it back for slash commands)
	            0x07: User whisper (to you)
	            0x08: Information
	            0x09: Error
	            0x0A: Broadcast from the server
	            0x0B: Battle.net Echo (the server wants you to send text to Battle.net)
	            0x0C: Quit (send it to formally request quit,
			            otherwise the server is politely telling you to close your connections)
	            0x0D: Fatal error (may be followed by 0x0C,
				        and usually means you did something the server did not like) */

            protocolLastChatEvent = DateTime.Now;
            switch (id)
            {
                case 0x01:
                    // This client is using a ghetto client that sends 0x01 to join channels
                    ProcessTextEvent("/join " + data);
                    break;
                case 0x06:
                    // Chat message
                    ProcessTextEvent(data);
                    break;
                case 0x0B:
                    // Echo request, here for potential processing, but the clients don't use this
                    ProcessTextEvent("/echo " + data);
                    break;
                case 0x0C:
                    Kill("User requested quit");
                    break;
                default:
                    Write("Unknown/refused chat event #" + id.ToString());
                    break;
            }
        }

        private void EnterChat()
        {
            Program.GetConsole.AddListItem(Username);
            RecognizeFlags();
            Write("Entering chat as " + Username + "...");
            int userLobby = Program.GetServer.LobbyID;
            if (IsAdmin())
                userLobby = Program.GetServer.AdminLobbyID;
            Program.GetServer.Channels[userLobby].JoinUser(this);
            SendPingRequest(0);
        }
        #endregion

        #region Recv Events
        private void RecvAccountCreate(string username, byte[] auth)
        {
            Write("Client request account creation for '" + username + "'...");
            byte response = 0x00;

            // Reserved words, its also possible that we load up additonal ones from file
            string[] reserved = new string[] { "admin", "operator", "lobby", "backstage" };

            // Get the auth note
            clsAuthNote note = clsAuth.GetAuth(username);

            // Check account exists
            if (note.exists)
                response = 0x01;

            // Check length
            if (!(username.Length >= 3 && username.Length <= 20))
                response = 0x02;

            // Check reserve
            foreach (string s in reserved)
            {
                if (username.ToLower().Contains(s))
                    response = 0x03;
            }

            // Decision
            if (response != 0x00)
            {
                // Creation failed, form a response
                switch (response)
                {
                    case 0x01:
                        Write("Account already exists!");
                        break;
                    case 0x02:
                        Write("Account name invalid length!");
                        break;
                    case 0x03:
                        Write("Account name contains invalid characters or words!");
                        break; 
                }

                // Reply
                SendAccountCreateResponse(response, "");
                return;
            }
            else
            {
                // Create
                clsAuth.SetAuth(username, auth);

                // Format controls
                Username = username;
                SendAccountCreateResponse(0x00, Username);
                SendLoginReponse(0x00, ""); // This is cross-compatability, its not necessary in the new client
                Write("Account created!");
                EnterChat();
            }
        }

        private void RecvIdent(string bnchan, string realmname, string game, int bnflag, int realm)
        {
            if (game.Length != 4)
            {
                Kill();
            }
            else
            {
                BN_Channel = bnchan;
                BN_Flags = bnflag;
                BN_Realm = realmname;
                BN_RealmID = realm;
                BN_Game = game;
                Write("Client using " + BN_Game + " on " + BN_Realm + " in channel " + BN_Channel);
            }
        }

        private void RecvKeepAlive()
        {
            protocolLastKeepAlive = DateTime.Now;
            SendData(new clsBuffer_Build().Build(0x00));
            Write("Test for echo...");
        }

        private void RecvLogin(string username, byte[] auth, string bn_username)
        {
            int checkExists = Program.GetServer.FindUser(username);

            BN_Username = bn_username;
            byte response = 0x00;

            Write("Authorizing account name '" + username + "'...");

            // Get the auth note
            clsAuthNote note = clsAuth.GetAuth(username);

            // Check account banned
            if (note.banned)
                response = 0x03;
            
            // Check account exists
            if (!note.exists)
                response = 0x02;

            // Check password correctness if exists & not banned
            if (response != 0x02 && response != 0x03)
            {
                // Check auth for correct password
                bool correct = clsAuth.CheckAuth(note.auth, auth);
                if (correct) // +
                    response = 0x00;
                else // -
                    response = 0x01;
            }

            bool allowEnterChat = false;

            // Decision
            switch (response)
            {
                case 0x00:
                    //Program.GetConsole.RemoveListItem(Username);
                    if (checkExists != -1)
                        Program.GetServer.FindUserObject(checkExists).Kill("Logged in twice");
                    Username = username;
                    Write("Client logged in as " + Username + " (" + BN_Username + ")!");
                    allowEnterChat = true;
                    break;
                case 0x01:
                    Write("Incorrect password!");
                    break;
                case 0x02:
                    Write("Account does not exist...");
                    break;
                case 0x03:
                    Write("User is banned.");
                    break;
            }

            SendLoginReponse(response, "");

            if (allowEnterChat)
                EnterChat();
        }

        private void RecvVersionInfo(string version, string bot, string plugin)
        {
            Write("Client using " + plugin + " on " + bot + ".");
        }

        private void RecvPing(int cookie)
        {
            if (WantPingResponse)
            {
                TimeSpan diff = (DateTime.Now - LastPingRequest);
                Ping = diff.Milliseconds;
                WantPingResponse = false;
                Program.GetConsole.AlterListItem(Username, LVFlags(), Ping, IPAddress);
            }
        }
        #endregion

        #region The Meat
        public int LastKA()
        {
            TimeSpan lastKA = (DateTime.Now - protocolLastKeepAlive);
            return lastKA.Seconds;
        }

        private void Process(short id, clsBuffer_Debuild packet)
        {
            switch (id)
            {
                case 0x00:
                    RecvKeepAlive();
                    break;
                case 0x01: // Auth
                    RecvLogin(packet.GetCString(), packet.GetBytes(20), packet.GetCString());
                    break;
                case 0x02: // Ident
                    string bnchan = packet.GetCString();
                    int bnflag = packet.GetDWord();
                    int realm = packet.GetDWord();
                    string realmname = packet.GetCString();
                    string game = packet.GetCString();
                    RecvIdent(bnchan, realmname, game, bnflag, realm);
                    break;
                case 0x03: // CreateAccount
                    RecvAccountCreate(packet.GetCString(), packet.GetBytes(20));
                    break;
                case 0x04: // ChatEvent
                    ProcessChatEvent(packet.GetDWord(), packet.GetCString());
                    break;
                case 0x05: // Version
                    RecvVersionInfo(packet.GetCString(), packet.GetCString(), packet.GetCString());
                    break;
                case 0x06: // Ping
                    RecvPing(packet.GetDWord());
                    break;
                default:
                    Write(" <-- 0x0" + id.ToString("X"));
                    break;
            }
        }

        private void Loop()
        {
            Thread.Sleep(100);
            byte[] header;
            byte[] data;
            int available = 0;

            while (true)
            {
                if (IsOnline())
                {
                    if (proto == 0x00)
                    {
                        if (connection.Available > 0)
                        {
                            byte[] protodata = new byte[1];
                            connection.Receive(protodata, 1, SocketFlags.None);
                            proto = protodata[0];
                            protocolSet = DateTime.Now;
                            switch (proto)
                            {
                                case 0x01:
                                    //Write("Client is using PyNet protocol");
                                    protocolName = "PyNet";
                                    protocolLastKeepAlive = DateTime.Now;
                                    protocolLastSwitchChannel = DateTime.Now;
                                    protocolLastChatEvent = DateTime.Now;
                                    break;
                                default:
                                    Write("Client requested invalid protocol");
                                    Kill();
                                    break;
                            }
                        }
                    }

                    // With apologies to Jessie Jackson
                    if (connection.Available >= 4 && available != connection.Available && proto == 0x01)
                    {
                        available = connection.Available;
                        while (available >= 4)
                        {
                            header = new byte[4];
                            connection.Receive(header, 4, SocketFlags.Peek);
                            byte leader = header[0];
                            byte id = header[1];
                            short length = BitConverter.ToInt16(header, 2);
                            if (available >= length && leader == 0xDD)
                            {
                                // This packet is complete.
                                data = new byte[length];
                                connection.Receive(data, length, SocketFlags.None);
                                //Write(" <-- 0x0" + id.ToString("X") + " (" + length.ToString() + "-len)");

                                clsBuffer_Debuild packet = new clsBuffer_Debuild();
                                packet.Unbuild(data);
                                Process(id, packet);

                                available = connection.Available;
                            }
                            else
                            {
                                // The client is sending some crazy shit. Wait for something else.
                                available = 0;
                            }
                        }
                    }
                    else
                    {
                        Thread.Sleep(20);
                    }
                }
            }
        }
        #endregion
    }
}
